package com.wmx.thymeleafapp.utils;

import sun.misc.BASE64Encoder;

import java.io.*;
import java.nio.MappedByteBuffer;
import java.nio.channels.FileChannel;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.util.Arrays;
import java.util.List;

/**
 * JAVA JDK 原生摘要工具类
 *
 * @author wangmaoxiong
 * @version 1.0
 * @date 2020/5/16 18:48
 */
@SuppressWarnings("Duplicates")
public class Md5EncodeUtils {

    // 支持的摘要算法列表（统一大写，便于校验）
    private static final List<String> SUPPORTED_ALGORITHMS = Arrays.asList("MD5", "SHA-1", "SHA-256");
    // 缓冲区大小（8KB，平衡内存占用与读取效率）
    private static final int BUFFER_SIZE = 8192;

    /**
     * 将任意字符通过 MD5 摘要 与 Base64 进行定长加密
     *
     * @param message   待加密字符
     * @param algorithm 信息生成摘要的算法，默认为  md5，可选值有  SHA-1、SHA-256、MD5
     * @return 加密后字符
     */
    public static String digestEncryption(String message, String algorithm) {
        String result = "";
        try {
            algorithm = algorithm == null ? "MD5" : algorithm;
            //指定信息摘要算法提取摘要的哈希值. 哈希值字节数组，如果直接 new String(md5Byte) 是会乱码的
            MessageDigest messageDigest = MessageDigest.getInstance(algorithm);
            byte[] md5Byte = messageDigest.digest(message.getBytes());
            //使用 BASE64 进行定长编码
            BASE64Encoder base64Encoder = new BASE64Encoder();
            result = base64Encoder.encode(md5Byte);
        } catch (NoSuchAlgorithmException e) {
            e.printStackTrace();
        }
        return result;
    }

    /**
     * 对文件提取摘要
     * 即使是大文件也没有任何问题，因为并非一次性读取整个文件的内容到内存，而是边读边更新摘要。
     *
     * @param file      待提取摘要的文件（不能为null、必须存在且为文件）
     * @param algorithm 生成摘要的算法（可选值：MD5、SHA-1、SHA-256，默认MD5）
     * @return 文件的16进制格式摘要字符串（小写）
     * @throws IllegalArgumentException 当文件无效或算法不支持时抛出
     * @throws NoSuchAlgorithmException 当指定算法在当前JVM中不支持时抛出（理论上支持列表内算法不会触发）
     * @throws IOException              当文件读取过程中发生IO错误时抛出（如权限不足、文件被删除）
     */
    public static String fileDigest(File file, String algorithm) throws IllegalArgumentException, NoSuchAlgorithmException, IOException {
        // 1. 校验文件合法性（无效文件直接抛异常，避免返回空字符串导致歧义）
        if (file == null) {
            throw new IllegalArgumentException("待处理文件不能为null");
        }
        if (!file.exists()) {
            throw new IllegalArgumentException("待处理文件不存在：" + file.getAbsolutePath());
        }
        if (!file.isFile()) {
            throw new IllegalArgumentException("指定路径不是文件：" + file.getAbsolutePath());
        }
        // 2. 处理算法参数（默认MD5，统一转为大写便于校验）
        String targetAlgorithm = (algorithm == null || algorithm.trim().isEmpty()) ? "MD5" : algorithm.trim().toUpperCase();
        // 校验算法是否在支持列表内
        if (!SUPPORTED_ALGORITHMS.contains(targetAlgorithm)) {
            throw new IllegalArgumentException(
                    "不支持的摘要算法：" + algorithm +
                            "，当前支持的算法为：" + String.join("、", SUPPORTED_ALGORITHMS)
            );
        }
        // 3. 基于NIO内存映射读取文件（高效处理大文件，避免传统流的频繁IO）
        try (FileInputStream fis = new FileInputStream(file);
             FileChannel fileChannel = fis.getChannel()) {
            MessageDigest messageDigest = MessageDigest.getInstance(targetAlgorithm);
            long fileSize = file.length();
            long currentPosition = 0;
            // 分块映射文件到内存（避免单次映射过大内存）
            while (currentPosition < fileSize) {
                // 计算当前块的实际大小（最后一块可能小于缓冲区大小）
                long blockSize = Math.min(BUFFER_SIZE, fileSize - currentPosition);
                // 内存映射文件的指定区域（只读模式）
                MappedByteBuffer mappedBuffer = fileChannel.map(FileChannel.MapMode.READ_ONLY, currentPosition, blockSize);
                // 用当前块数据更新摘要
                messageDigest.update(mappedBuffer);
                // 更新下一块的起始位置
                currentPosition += blockSize;
            }
            byte[] digestBytes = messageDigest.digest();
            // 将字节数组转换为十六进制字符串
            StringBuilder sb = new StringBuilder();
            for (byte b : digestBytes) {
                sb.append(String.format("%02x", b));
            }
            return sb.toString();
        }
    }

    /**
     * 重载方法：默认使用MD5算法提取文件摘要
     * 即使是大文件也没有任何问题，因为并非一次性读取整个文件的内容到内存，而是边读边更新摘要。
     *
     * @param file 待处理文件
     * @return MD5摘要的16进制字符串（小写）
     * @throws IllegalArgumentException 文件无效时抛出
     * @throws NoSuchAlgorithmException 算法不支持时抛出
     * @throws IOException              读取文件失败时抛出
     */
    public static String fileDigest(File file) throws IllegalArgumentException, NoSuchAlgorithmException, IOException {
        return fileDigest(file, "MD5");
    }

    /**
     * 对文件提取摘要。
     * 应用场景：文件一致性较验
     * 即使是大文件也没有任何问题，因为并非一次性读取整个文件的内容到内存，而是边读边更新摘要。
     *
     * @param file ：待提取摘要的文件
     * @return
     */
    public static String fileDigestV2(File file) throws Exception {
        // 先检查文件是否存在
        if (file == null || !file.exists() || !file.isFile()) {
            return "";  // 先检查文件是否存在
        }
        try (InputStream inputStream = new FileInputStream(file)) {
            MessageDigest md = MessageDigest.getInstance("MD5");
            byte[] buffer = new byte[8192];
            int bytesRead;
            // 读取文件内容并更新消息摘要
            while ((bytesRead = inputStream.read(buffer)) != -1) {
                md.update(buffer, 0, bytesRead);
            }
            // 获取MD5摘要字节数组
            byte[] md5Bytes = md.digest();
            // 将字节数组转换为十六进制字符串
            StringBuilder sb = new StringBuilder();
            for (byte b : md5Bytes) {
                sb.append(String.format("%02x", b));
            }
            return sb.toString();
        }
    }

    public static void main(String[] args) throws Exception {
        new Md5EncodeUtils().test1();
        new Md5EncodeUtils().test2();
        new Md5EncodeUtils().test3();
    }

    public void test3() throws Exception {
        File file = new File("F:\\Music\\1987我不知会遇见你 - 李宇春.mp3");
        String fileMd5Digest = fileDigestV2(file);
        System.out.println("fileMd5Digest: " + fileMd5Digest.toLowerCase());
    }

    public void test2() throws Exception {
        File file = new File("F:\\Music\\1987我不知会遇见你 - 李宇春.mp3");
        String fileMd5Digest = fileDigest(file);
        System.out.println("fileMd5Digest: " + fileMd5Digest.toLowerCase());
    }

    public void test1() {
        String sourceMessage = "123456万里长城_&*$#.Nice";
        //原字符：123456万里长城_&*$#.Nice
        System.out.println("原字符：" + sourceMessage);

        String md5Msg = digestEncryption(sourceMessage, "md5");
        String sha1Msg = digestEncryption(sourceMessage, "SHA-1");
        String sha256Msg = digestEncryption(sourceMessage, "SHA-256");
        //md5 + base64 加密后：rIJQEL19bo+eV5p7qPLlDg==
        System.out.println("md5 + base64 加密后：" + md5Msg);
        //md5 + base64 加密后：UYg5qBdsuCdloFP+0CVPasziIEU=
        System.out.println("SHA-1 + base64 加密后：" + sha1Msg);
        //md5 + base64 加密后：dqTa6BTTvZ4zLF5WNAH5Cv660RxYMEIJlBRIGKdNXmM=
        System.out.println("SHA-256 + base64 加密后：" + sha256Msg);
    }
}

